/**
 * Copyright 2009-2019 the original author or authors.
 * <p>
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.ibatis.builder.xml;

import org.apache.ibatis.builder.BaseBuilder;
import org.apache.ibatis.builder.MapperBuilderAssistant;
import org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator;
import org.apache.ibatis.executor.keygen.KeyGenerator;
import org.apache.ibatis.executor.keygen.NoKeyGenerator;
import org.apache.ibatis.executor.keygen.SelectKeyGenerator;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ResultSetType;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.mapping.StatementType;
import org.apache.ibatis.parsing.XNode;
import org.apache.ibatis.scripting.LanguageDriver;
import org.apache.ibatis.session.Configuration;

import java.util.List;
import java.util.Locale;

/**
 * 用于解析mapper中的sql节点
 * 这里的sql节点指的是
 * select、update、insert、delete节点
 *
 * @author Clinton Begin
 */
public class XMLStatementBuilder extends BaseBuilder {

    private final MapperBuilderAssistant builderAssistant;

    /**
     * 当前sql的节点
     */
    private final XNode context;
    private final String requiredDatabaseId;

    public XMLStatementBuilder(Configuration configuration, MapperBuilderAssistant builderAssistant, XNode context) {
        this(configuration, builderAssistant, context, null);
    }

    public XMLStatementBuilder(Configuration configuration, MapperBuilderAssistant builderAssistant, XNode context, String databaseId) {
        super(configuration);
        this.builderAssistant = builderAssistant;
        this.context = context;
        this.requiredDatabaseId = databaseId;
    }

    /**
     * 解析sql节点
     */
    public void parseStatementNode() {
        // 当前节点id
        String id = context.getStringAttribute("id");
        // 获取数据库id
        String databaseId = context.getStringAttribute("databaseId");
        // 启用的数据库和sql节点配置的不同
        if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
            return;
        }
        // 获取当前节点的名称
        String nodeName = context.getNode().getNodeName();
        // 获取到sql的类型。select|update|delete|insert
        SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
        boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
        boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
        boolean useCache = context.getBooleanAttribute("useCache", isSelect);
        boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);

        // 解析sql前先处理include节点。
        XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
        includeParser.applyIncludes(context.getNode());

        // 获取parameterType属性
        String parameterType = context.getStringAttribute("parameterType");
        // 直接拿到parameterType对应的Class
        Class<?> parameterTypeClass = resolveClass(parameterType);
        // 获取到lang属性
        String lang = context.getStringAttribute("lang");
        // 获取对应的动态sql语言驱动器。
        LanguageDriver langDriver = getLanguageDriver(lang);

        // 解析selectKey节点
        processSelectKeyNodes(id, parameterTypeClass, langDriver);

        KeyGenerator keyGenerator;
        // 拼接id。形如findById!selectKey
        String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
        // 给这个id前面追加当前的命名空间
        keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
        if (configuration.hasKeyGenerator(keyStatementId)) {
            keyGenerator = configuration.getKeyGenerator(keyStatementId);
        } else {
            // 优先取配置的useGeneratorKeys。如果为空就判断当前配置是否允许jdbc自动生成主键，并且当前是insert语句
            // 判断如果为真就创建Jdbc3KeyGenerator，如果为假就创建NoKeyGenerator
            keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
                    configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
                    ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
        }

        // 获取当前sql节点的一堆属性，去创建MappedStatement。
        // 这里创建的MappedStatement就代表一个sql节点
        // 也是后面编写mybatis拦截器时可以拦截的一处
        // TODO createSqlSource to XMLLanguageDriver.createSqlSource
        SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
        StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
        Integer fetchSize = context.getIntAttribute("fetchSize");
        Integer timeout = context.getIntAttribute("timeout");
        String parameterMap = context.getStringAttribute("parameterMap");
        String resultType = context.getStringAttribute("resultType");
        Class<?> resultTypeClass = resolveClass(resultType);
        String resultMap = context.getStringAttribute("resultMap");
        String resultSetType = context.getStringAttribute("resultSetType");
        ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
        if (resultSetTypeEnum == null) {
            resultSetTypeEnum = configuration.getDefaultResultSetType();
        }
        String keyProperty = context.getStringAttribute("keyProperty");
        String keyColumn = context.getStringAttribute("keyColumn");
        String resultSets = context.getStringAttribute("resultSets");

        builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
                fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
                resultSetTypeEnum, flushCache, useCache, resultOrdered,
                keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
    }

    /**
     * 解析selectKey节点
     * selectKey节点可以解决insert时主键自增问题
     * 如果需要在插入数据后获取到主键，就需要使用selectKey节点
     *
     * @param id                 sql节点的id
     * @param parameterTypeClass 参数类型
     * @param langDriver         动态sql语言驱动器
     */
    private void processSelectKeyNodes(String id, Class<?> parameterTypeClass, LanguageDriver langDriver) {
        // 获取全部的selectKey节点
        List<XNode> selectKeyNodes = context.evalNodes("selectKey");
        if (configuration.getDatabaseId() != null) {
            parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, configuration.getDatabaseId());
        }
        parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, null);
        removeSelectKeyNodes(selectKeyNodes);
    }

    /**
     * 解析selectKey节点
     *
     * @param parentId             父节点id（指sql节点的id）
     * @param list                 所有的selectKey节点
     * @param parameterTypeClass   参数类型
     * @param langDriver           动态sql驱动
     * @param skRequiredDatabaseId 数据源id
     */
    private void parseSelectKeyNodes(String parentId, List<XNode> list, Class<?> parameterTypeClass, LanguageDriver langDriver, String skRequiredDatabaseId) {
        // 遍历selectKey节点
        for (XNode nodeToHandle : list) {
            // 拼接id 修改为形如 findById!selectKey形式
            String id = parentId + SelectKeyGenerator.SELECT_KEY_SUFFIX;
            // 获得当前节点的databaseId属性
            String databaseId = nodeToHandle.getStringAttribute("databaseId");
            // 只解析databaseId是当前启用databaseId的节点
            if (databaseIdMatchesCurrent(id, databaseId, skRequiredDatabaseId)) {
                parseSelectKeyNode(id, nodeToHandle, parameterTypeClass, langDriver, databaseId);
            }
        }
    }

    /**
     * 解析selectKey节点
     *
     * @param id                 节点id
     * @param nodeToHandle       selectKey节点
     * @param parameterTypeClass 参数类型
     * @param langDriver         动态sql驱动
     * @param databaseId         数据库id
     */
    private void parseSelectKeyNode(String id, XNode nodeToHandle, Class<?> parameterTypeClass, LanguageDriver langDriver, String databaseId) {
        // 获取 resultType 属性
        String resultType = nodeToHandle.getStringAttribute("resultType");
        // 解析返回值类型
        Class<?> resultTypeClass = resolveClass(resultType);
        // 解析statementType（sql类型，简单sql、动态sql、存储过程）
        StatementType statementType = StatementType.valueOf(nodeToHandle.getStringAttribute("statementType", StatementType.PREPARED.toString()));
        // 获取keyProperty和keyColumn属性
        String keyProperty = nodeToHandle.getStringAttribute("keyProperty");
        String keyColumn = nodeToHandle.getStringAttribute("keyColumn");
        // 是在之前还是之后去获取主键
        boolean executeBefore = "BEFORE".equals(nodeToHandle.getStringAttribute("order", "AFTER"));

        // 设置MappedStatement对象需要的一系列属性默认值
        boolean useCache = false;
        boolean resultOrdered = false;
        KeyGenerator keyGenerator = NoKeyGenerator.INSTANCE;
        Integer fetchSize = null;
        Integer timeout = null;
        boolean flushCache = false;
        String parameterMap = null;
        String resultMap = null;
        ResultSetType resultSetTypeEnum = null;

        // TODO 生成sqlSource 这里调用的是XMLLanguageDriver.createSqlSource，这里会解析动态sql
        SqlSource sqlSource = langDriver.createSqlSource(configuration, nodeToHandle, parameterTypeClass);
        // selectKey节点只能配置select语句
        SqlCommandType sqlCommandType = SqlCommandType.SELECT;

        // 用这么一大坨东西去创建MappedStatement对象并添加到Configuration中
        builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
                fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
                resultSetTypeEnum, flushCache, useCache, resultOrdered,
                keyGenerator, keyProperty, keyColumn, databaseId, langDriver, null);

        // 启用当前命名空间（给id前面加上命名空间）
        id = builderAssistant.applyCurrentNamespace(id, false);
        // 从Configuration中拿到上面的MappedStatement
        MappedStatement keyStatement = configuration.getMappedStatement(id, false);
        configuration.addKeyGenerator(id, new SelectKeyGenerator(keyStatement, executeBefore));
    }

    private void removeSelectKeyNodes(List<XNode> selectKeyNodes) {
        for (XNode nodeToHandle : selectKeyNodes) {
            nodeToHandle.getParent().getNode().removeChild(nodeToHandle.getNode());
        }
    }

    private boolean databaseIdMatchesCurrent(String id, String databaseId, String requiredDatabaseId) {
        if (requiredDatabaseId != null) {
            return requiredDatabaseId.equals(databaseId);
        }
        if (databaseId != null) {
            return false;
        }
        id = builderAssistant.applyCurrentNamespace(id, false);
        if (!this.configuration.hasStatement(id, false)) {
            return true;
        }
        // skip this statement if there is a previous one with a not null databaseId
        MappedStatement previous = this.configuration.getMappedStatement(id, false); // issue #2
        return previous.getDatabaseId() == null;
    }

    private LanguageDriver getLanguageDriver(String lang) {
        Class<? extends LanguageDriver> langClass = null;
        if (lang != null) {
            langClass = resolveClass(lang);
        }
        return configuration.getLanguageDriver(langClass);
    }

}
